#include <SFML/Graphics/Vertex.hpp>
#include <SFML/Graphics/VertexArray.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
#include <nice2d/particle.h>
#include <nice2d/colorgradient.h>
#include <nice2d/util.h>

class ParticleEmitterData
{
public:
    std::size_t computeParticleCount(float deltaTime)
    {
        float particleAmount = particlesPerSecond * deltaTime + emissionDifference;
        std::size_t nbParticles = static_cast<std::size_t>(particleAmount);

        if (nbParticles == 0)
            emissionDifference += particleAmount;
        else
            emissionDifference = particleAmount - nbParticles;

        return nbParticles;
    }

    float particlesPerSecond = 30.0f;
    std::function<float()> particleLifetime;
    std::function<sf::Vector2f()> particlePosition;
    std::function<sf::Vector2f()> particleVelocity;
    std::function<sf::Vector2f()> particleAccelate;
    std::function<float()> particleRotation;
    std::function<float()> particleRotationSpeed;
    std::function<sf::Vector2f()> particleScale;
    std::function<sf::Color()> particleColor;
    std::function<uint32_t()> particleTextureIndex;
    float emissionDifference = 0.0f;
};

ParticleEmitter::ParticleEmitter():
    data(new ParticleEmitterData())
{
}

ParticleEmitter::~ParticleEmitter()
{
}

void ParticleEmitter::setEmissionRate(float particlesPerSecond)
{
    data->particlesPerSecond = particlesPerSecond;
}

void ParticleEmitter::setParticleLifetime(std::function<float ()> particleLifetime)
{
    data->particleLifetime = particleLifetime;
}

void ParticleEmitter::setParticlePosition(std::function<sf::Vector2f ()> particlePosition)
{
    data->particlePosition = particlePosition;
}

void ParticleEmitter::setParticleVelocity(std::function<sf::Vector2f ()> particleVelocity)
{
    data->particleVelocity = particleVelocity;
}

void ParticleEmitter::setParticleAccelerate(std::function<sf::Vector2f ()> particleAccelate)
{
    data->particleAccelate = particleAccelate;
}

void ParticleEmitter::setParticleRotation(std::function<float ()> particleRotation)
{
    data->particleRotation = particleRotation;
}

void ParticleEmitter::setParticleRotationSpeed(std::function<float ()> particleRotationSpeed)
{
    data->particleRotationSpeed = particleRotationSpeed;
}

void ParticleEmitter::setParticleScale(std::function<sf::Vector2f ()> particleScale)
{
    data->particleScale = particleScale;
}

void ParticleEmitter::setParticleColor(std::function<sf::Color ()> particleColor)
{
    data->particleColor = particleColor;
}

void ParticleEmitter::setParticleTextureIndex(std::function<uint32_t ()> particleTextureIndex)
{
    data->particleTextureIndex = particleTextureIndex;
}

void ParticleEmitter::operator()(AbstractEmitter *emitter, float deltaTime)
{
    if (!emitter)
        return;

    const std::size_t count = data->computeParticleCount(deltaTime);

    for (std::size_t i = 0; i < count; ++i) {
        float lifeTime = data->particleLifetime ? data->particleLifetime() : 10.0f;

        Particle particle(lifeTime);
        particle.position = data->particlePosition();
        if (data->particleVelocity)
            particle.velocity = data->particleVelocity();
        if (data->particleAccelate)
            particle.velocity += data->particleAccelate() * deltaTime;
        if (data->particleRotation)
            particle.rotation = data->particleRotation();
        if (data->particleRotationSpeed)
            particle.rotationSpeed = data->particleRotationSpeed();
        if (data->particleScale)
            particle.scale = data->particleScale();
        if (data->particleColor)
            particle.color = data->particleColor();
        if (data->particleTextureIndex)
            particle.textureIndex = data->particleTextureIndex();

        particle.startScale = particle.scale;
        particle.startColor = particle.color;

        emitter->emitParticle(particle);
    }
}

ParticleForceAffector::ParticleForceAffector(sf::Vector2f inputAcceleration):
    acceleration(inputAcceleration)
{
}

void ParticleForceAffector::operator()(Particle &particle, float deltaTime)
{
    particle.velocity += deltaTime * acceleration;
}

ParticleTorqueAffector::ParticleTorqueAffector(float inputAngularAcceleration):
    angularAcceleration(inputAngularAcceleration)
{
}

void ParticleTorqueAffector::operator()(Particle &particle, float deltaTime)
{
    particle.rotationSpeed += deltaTime * angularAcceleration;
}

ParticleScaleAffector::ParticleScaleAffector(sf::Vector2f inputScaleFactor):
    scaleFactor(inputScaleFactor)
{
}

void ParticleScaleAffector::operator()(Particle &particle, float deltaTime)
{
    float p = particle.elapseLifetime / particle.totalLifetime;
    auto targetScale = sf::Vector2f(scaleFactor.x * particle.startScale.x,
                                    scaleFactor.y * particle.startScale.y);
    particle.scale = interpolate(particle.startScale, targetScale, 1 - p);
}

ParticleAlphaAffector::ParticleAlphaAffector(float inputAlpha):
    alpha(inputAlpha)
{
}

void ParticleAlphaAffector::operator()(Particle &particle, float deltaTime)
{
    float p = (1 - (particle.totalLifetime - particle.elapseLifetime) /
               particle.totalLifetime);
    p = clamp(p, 0.0f, 1.0f);
    particle.color.a = p * particle.startColor.a + (1 - p) * alpha;
}

ParticleColorAffector::ParticleColorAffector(const sf::Color &inputColor):
    color(inputColor)
{
}

void ParticleColorAffector::operator()(Particle &particle, float deltaTime)
{
    float value = particle.elapseLifetime / particle.totalLifetime;
    particle.color = blendColors(particle.startColor, color, value);
}

ParticleProxyAffector::ParticleProxyAffector(std::function<void (Particle &, float)>
                                             inputFunction):
    function(inputFunction)
{
}

void ParticleProxyAffector::operator()(Particle &particle, float deltaTime)
{
    if (function)
        function(particle, deltaTime);
}

class ParticleSystemData
{
public:
    ParticleSystemData():
        vertices(sf::Quads)
    {
    }

    void computeQuad(Quad &quad, const sf::IntRect &textureRect) const
    {
        sf::FloatRect rect(textureRect);

        quad[0].texCoords = sf::Vector2f(rect.left,              rect.top);
        quad[1].texCoords = sf::Vector2f(rect.left + rect.width, rect.top);
        quad[2].texCoords = sf::Vector2f(rect.left + rect.width, rect.top + rect.height);
        quad[3].texCoords = sf::Vector2f(rect.left,              rect.top + rect.height);

        quad[0].position = sf::Vector2f(-rect.width, -rect.height) / 2.f;
        quad[1].position = sf::Vector2f( rect.width, -rect.height) / 2.f;
        quad[2].position = sf::Vector2f( rect.width,  rect.height) / 2.f;
        quad[3].position = sf::Vector2f(-rect.width,  rect.height) / 2.f;
    }

    sf::IntRect getFullRect(const sf::Texture &texture)const
    {
        return sf::IntRect(0, 0, texture.getSize().x, texture.getSize().y);
    }

    void computeQuads() const
    {
        assert(texture);

        if (textureRects.empty()) {
            quads.resize(1);
            computeQuad(quads[0], getFullRect(*texture));
        }

        else {
            quads.resize(textureRects.size());
            for (std::size_t i = 0; i < textureRects.size(); ++i)
                computeQuad(quads[i], textureRects[i]);
        }
    }

    void computeVertices() const
    {
        vertices.clear();

        std::for_each(particles.begin(), particles.end(), [&](const Particle & particle) {
            sf::Transform transform;
            transform.translate(particle.position);
            transform.rotate(particle.rotation);
            transform.scale(particle.scale);

            assert(particle.textureIndex == 0 || particle.textureIndex < textureRects.size());

            const auto &quad = quads[particle.textureIndex];
            for (std::size_t i = 0; i < 4; ++i) {
                sf::Vertex vertex;
                vertex.position = transform.transformPoint(quad[i].position);
                vertex.texCoords = quad[i].texCoords;
                vertex.color = particle.color;

                vertices.append(vertex);
            }
        });
    }

    sf::Texture *texture = nullptr;
    std::shared_ptr<ParticleEmitter> emitter;
    std::vector<std::shared_ptr<ParticleAffector>> affectors;

    std::vector<sf::IntRect> textureRects;

    mutable sf::VertexArray vertices;
    mutable bool needsVertexUpdate = true;
    mutable std::vector<Quad> quads;
    mutable bool needsQuadUpdate = true;

    std::list<Particle> particles;
};

ParticleSystem::ParticleSystem():
    data(new ParticleSystemData())
{
}

ParticleSystem::~ParticleSystem()
{
}

void ParticleSystem::setTexture(sf::Texture *texture)
{
    if (texture) {
        data->texture = texture;
        data->needsQuadUpdate = true;
    }
}

uint32_t ParticleSystem::addTextureRect(const sf::IntRect &textureRect)
{
    data->textureRects.push_back(textureRect);
    data->needsQuadUpdate = true;

    return static_cast<uint32_t>(data->textureRects.size() - 1);
}

void ParticleSystem::setParticleEmitter(std::shared_ptr<ParticleEmitter> emitter)
{
    if (emitter)
        data->emitter = emitter;
}

std::shared_ptr<ParticleEmitter> ParticleSystem::getParticleEmitter() const
{
    return data->emitter;
}

void ParticleSystem::addAffector(std::shared_ptr<ParticleAffector> affector)
{
    if (affector)
        data->affectors.push_back(affector);
}

void ParticleSystem::clearAffectors()
{
    data->affectors.clear();
}

void ParticleSystem::emitParticle(const Particle &particle)
{
    data->particles.push_back(particle);
}

void ParticleSystem::clearParticles()
{
    data->particles.clear();
}

void ParticleSystem::onDrawNode(sf::RenderTarget &target, sf::RenderStates states) const
{
    if (data->needsQuadUpdate) {
        data->computeQuads();
        data->needsQuadUpdate = false;
    }

    if (data->needsVertexUpdate) {
        data->computeVertices();
        data->needsVertexUpdate = false;
    }

    states.transform *= getTransform();
    states.texture = data->texture;
    target.draw(data->vertices, states);
}

void ParticleSystem::onUpdateNode(float deltaTime)
{
    data->needsVertexUpdate = true;
    data->emitter->operator()(this, deltaTime);

    std::for_each(data->particles.begin(), data->particles.end(), [&](Particle & particle) {
        for (auto affector : data->affectors) {
            affector->operator()(particle, deltaTime);
        }
        particle.position += particle.velocity * deltaTime;
        particle.rotation += particle.rotationSpeed * deltaTime;
        particle.elapseLifetime += deltaTime;
    });

    auto itr = std::remove_if(data->particles.begin(),
                              data->particles.end(), [](const Particle & particle)->bool{return particle.elapseLifetime >= particle.totalLifetime;});
    data->particles.erase(itr, data->particles.end());
}

